"""
================================================================================

    abc2esac - ABC to EsAC conversion
    Copyright (C) 2010, Marcus Weseloh

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.


    Some code in this module was tanken from the Mingus project. See
    http://code.google.com/p/minugus/ for more details.

================================================================================
"""

from fractions import Fraction as F

BASE_LENGHTS = [F(1, 128), F(1, 64), F(1, 32), F(1, 16), F(1, 8), F(1, 4),
                F(1, 2), F(1), F(2), F(4)]
NOTE_DICT = {'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11}
FIFTHS = ['F', 'C', 'G', 'D', 'A', 'E', 'B']

class Key(object):
    _key_cache = {}

    def __init__(self, key):
        if not is_valid_note(key):
            raise Exception, "Unrecognised format for key '%s'" % key
        self.key = key

    def get_notes(self):
        if self.key in self._key_cache:
            return self._key_cache[self.key]

        fifth_index = FIFTHS.index(self.key[0])
        result = []

        # fifth_index = 0 is a special case. It's the key of F and needs 
        # Bb instead of B included in the result.
        if fifth_index != 0:
            result.append(FIFTHS[(fifth_index - 1) % 7] + self.key[1:])
            for x in FIFTHS[fifth_index:]:
                result.append(x  + self.key[1:])
            for x in FIFTHS[:(fifth_index - 1)]:
                result.append(x + self.key[1:] + "#")
        else:
            for x in FIFTHS[0:6]:
                result.append(x + self.key[1:])
            result.append("Bb" + self.key[1:])

        result.sort()

        # Remove redundant #'s and b's from the result
        result = map(remove_redundant_accidentals, result)
        tonic = result.index(remove_redundant_accidentals(self.key))

        result = result[tonic:] + result[:tonic]

        # Save result to cache
        self._key_cache[self.key] = result
        return result

    def interval(self, start_note, interval):
        """Returns the note found at the interval starting from start_note
        in the given key. For example interval('C', 'D', 1) will return 'E'.
        """
        if not is_valid_note(start_note):
            raise Exception("The start note '%s' is not a valid note" %
                            start_note)

        notes_in_key = self.get_notes()

        for n in notes_in_key:
            if n[0] == start_note[0]:
                index = notes_in_key.index(n)
        return notes_in_key[(index + interval) % 7]

    def __repr__(self):
        return 'Key: %s' % self.key

    def mode_scale(self, mode):
        if mode == 'major' or mode == 'ionian':
            return self.get_notes()
        elif mode == 'minor':
            trd = Key('C').interval(self.key[0], 2) # third
            note = minor_major_helper(self.key, trd, 3)
            s = Key(note).get_notes()
            return s[5:] + s[:5]
        elif mode == 'dorian':
            sth = Key('C').interval(self.key[0], 6) # seventh
            note = minor_major_helper(self.key, sth, 10)
            i = Key(note).get_notes()
            return i[1:] + [i[0]]
        elif mode == 'phrygian':
            sth = Key('C').interval(self.key[0], 5) # sixth
            note =  minor_major_helper(self.key, sth, 8)
            i = Key(note).get_notes()
            return i[2:] + i[:2]
        elif mode == 'lydian':
            tmp = Key('C').interval(self.key[0], 4) # fifth
            note =  minor_major_helper(self.key, tmp, 7)
            i = Key(note).get_notes()
            return i[3:] + i[:3]
        elif mode == 'mixolydian':
            tmp = Key('C').interval(self.key[0], 3) # fourth
            note =  minor_major_helper(self.key, tmp, 5)
            i = Key(note).get_notes()
            return i[4:] + i[:4]
        elif mode == 'aeolian':
            tmp = Key('C').interval(self.key[0], 2) # third
            note =  minor_major_helper(self.key, tmp, 3) # minor third
            i = Key(note).get_notes()
            return i[5:] + i[:5]
        elif mode == 'locrian':
            tmp = Key('C').interval(self.key[0], 1) # second
            note =  minor_major_helper(self.key, tmp, 1) # minor second
            i = Key(note).get_notes()
            return i[6:] + i[:6]

class Element(object):
    def __init__(self, length=None, dots=0):
        self.length = length or F(1, 4)
        self.dots = dots
        self.tie = False

    def clone(self):
        return self.__class__(self.length)

    def set_length(self, length):
        self.length, self.dots = length_value(length)

    def get_relative_length(self, base_length):
        try:
            base_idx = BASE_LENGHTS.index(base_length)
            length_idx = BASE_LENGHTS.index(self.length)
        except ValueError:
            raise Exception('unable to get relative length for %s to base %s' %
                            (self.length, base_length))
        return length_idx - base_idx

    def get_total_length(self):
        if self.dots:
            length = self.length
            additional_length = self.length / 2
            for i in range(self.dots):
                length += additional_length
                additional_length /= 2
            return length
        else:
            return self.length

class Bar(object):
    def __init__(self, meter, start_repeat=False, end_repeat=False,
                 nth_repeat=None):
        self.meter = meter
        self.start_repeat = start_repeat
        self.end_repeat = end_repeat
        self.nth_repeat = nth_repeat
        self.elements = []

    def clone(self):
        bar = Bar(self.meter, self.start_repeat, self.end_repeat)
        for el in self.elements:
            bar.elements.append(el.clone())
        return bar

    def add_element(self, element):
        if self.get_length() + element.get_total_length() > \
           F(self.meter[0], self.meter[1]):
            raise Exception('Overfull bar! Bar contents: %s' % self.elements)
        self.elements.append(element)

    def get_length(self):
        return sum([e.get_total_length() for e in self.elements])

    def space_left(self):
        return F(self.meter[0], self.meter[1]) - self.get_length() 

    def is_full(self):
        return self.get_length() >= F(self.meter[0], self.meter[1])

    def note_list(self):
        """Return a flat list of notes, and rests. Tuples are flattened."""
        notes = []
        for el in self.elements:
            if isinstance(el, Tuplet):
                notes.extend(el.get_contents())
            else:
                notes.append(el)
        return notes

class Rest(Element):
    def __repr__(self):
        return 'Rest: %s' % self.length

class Note(Element):
    def __init__(self, length=None, name='C', octave=4, tie=False, dots=0):
        self.name = name
        self.octave = octave
        self.tie = tie
        super(Note, self).__init__(length, dots)

    def clone(self):
        return Note(self.length, self.name, self.octave, self.tie)

    def augment(self):
        self.name = augment_note(self.name)

    def diminish(self):
        self.name = diminish_note(self.name)

    def octave_up(self):
        self.change_octave(1)

    def octave_down(self):
        self.change_octave(-1)

    def change_octave(self, diff):
        self.octave += diff
        if self.octave < 0:
            self.octave = 0

    def get_midi_pitch(self):
        idx = NOTE_DICT[self.name[0]]
        if len(self.name) > 1:
            if self.name[1] == 'b':
                idx -= 1
            elif self.name[1] == '#':
                idx += 1
        return ((self.octave + 1) * 12) + idx

    def __repr__(self):
        return '%s-%d %s%s%s' % (self.name, self.octave,
                                 self.length, '.' * self.dots,
                                 self.tie and ' --' or '')

class Tuplet(Element):
    def __init__(self, tuplet_count, base_count, notes=None):
        self.tuplet_count = tuplet_count
        self.base_count = base_count
        self.notes = notes or []

    def add_note(self, note):
        self.notes.append(note)

    def get_contents(self):
        contents = []
        for note in self.notes:
            n = note.clone()
            n.length = n.length * F(self.base_count, self.tuplet_count)
            n.dots = 0
            contents.append(n)
        return contents

    def __repr__(self):
        return '(%s)' % ', '.join((str(n) for n in self.notes))

    def _get_length(self):
        return sum([n.length * F(self.base_count, self.tuplet_count)
                    for n in self.notes])
    length = property(_get_length)

    def get_total_length(self):
        return sum([n.get_total_length() * F(self.base_count, self.tuplet_count)
                    for n in self.notes]) 

class Song(object):
    def __init__(self):
        self.index = 0
        self.title = None
        self.composer = None
        self.origin = None
        self.discography = None
        self.tempo = None
        self.rhythm = None
        self.source = None
        self.notes = None
        self.history = None
        self.information = None
        self.group = None
        self.book = None
        self.area = None
        self.mode = 'ionian'
        self.transscrnotes = None
        self.key = 'C'
        self.meter = (4, 4)
        self.base_length = F('1/4')
        self.bars = []

    def resolve_repeats(self):
        bars = []
        include_2nd_repeat = False
        skip_bars = 0
        num_bars = len(self.bars)
        for bar_idx, bar in enumerate(self.bars):
            if skip_bars > 0:
                skip_bars -= 1
                continue
            if bar.nth_repeat != 2 or include_2nd_repeat:
                include_2nd_repeat = False
                bars.append(bar)
            if bar.end_repeat:
                for i in range(bar_idx, 0, -1):
                    if self.bars[i-1].start_repeat:
                        break

                # handle weird repeat-upbeat common to irish abc notation
                if not bar.is_full():
                    if self.bars[i-1].get_length() == bar.space_left():
                        # upbeat at start of repeat
                        orig_bar = bar.clone()
                        # add elements of upbeat bar to current bar
                        for el in self.bars[i-1].elements:
                            bar.add_element(el)
                        # repeat all bars, except for upbeat and current bar
                        for b in self.bars[i:bar_idx]:
                            if b.nth_repeat != 1:
                                bars.append(b)
                        # lengthen last note of original bar to fill up bar-time
                        orig_bar.elements[-1].set_length(
                            orig_bar.space_left() +
                            orig_bar.elements[-1].get_total_length())
                        # and add it to the bar list
                        bars.append(orig_bar)
                    elif bar_idx < (num_bars - 1) and \
                            self.bars[bar_idx+1].get_length() == \
                            bar.space_left():
                        # upbeat after repeat
                        orig_bar = bar.clone()
                        # lengthen last note of current bar to fill up bar-time
                        bar.elements[-1].set_length(
                            bar.space_left() + 
                            bar.elements[-1].get_total_length())
                        # repeat all bars except for current bar 
                        for b in self.bars[i-1:bar_idx]:
                            if b.nth_repeat != 1:
                                bars.append(b)
                        # add elements from upbeat bar to original bar
                        for el in self.bars[bar_idx+1].elements:
                            orig_bar.add_element(el)
                        # and add it to list
                        bars.append(orig_bar)
                        # then skip the next bar (the upbeat bar)
                        skip_bars = 1

                else:
                    for b in self.bars[i-1:bar_idx+1]:
                        if b.nth_repeat != 1:
                            bars.append(b)
                    if bar.nth_repeat == 1:
                        include_2nd_repeat = True
        self.bars = bars

    def _get_elements(self):
        elements = []
        for bar in self.bars:
            elements.extend(bar.elements)
        return elements
    elements = property(_get_elements)

    def get_tonic(self):
        return self.key
        if self.mode == 'minor':
            return to_major(self.key)
        else:
            return self.key

    def get_shortest_length(self):
        try:
            return min((el.length for el in self.elements 
                        if hasattr(el, 'length')))
        except ValueError:
            raise Exception('no elements in %s (%s, %s)' %
                            (self, self.title, self.elements))

def length_value(length):
    """Return the base note length and the numer of dots"""
    base_length, rest = base_length_rest(length)
    # print 'length: %s, base: %s, rest: %s' % (length, base_length, rest)
    if rest == 0:
        return (base_length, 0)
    base, rest = base_length_rest(rest)
    if rest == 0:
        return (base_length, 1)
    base, rest = base_length_rest(rest)
    if rest == 0:
        return (base_length, 2)
    base, rest = base_length_rest(rest)
    if rest == 0:
        return (base_length, 3)
    raise Exception('Hm... %s, %s, %s, %s' % (length, base, rest, base_length))

def base_length_rest(length):
    for i, val in enumerate(BASE_LENGHTS):
        if length == val:
            return (val, 0)
        if length < val:
            val = BASE_LENGHTS[i-1]
            return (val, length - val)
    return (val, 0)


def is_valid_note(note):
    """Returns true if note is in a recognised format. False if not"""
    if note[0] not in NOTE_DICT:
        return False
    for post in note[1:]:
        if post != 'b' and post != '#':
            return False
    return True

def note_to_int(note):
    """Converts notes in the form of C, C#, Cb, C##, etc. to
    an integer in the range of 0-11."""
    if is_valid_note(note):
        val = NOTE_DICT[note[0]]
    else:
        raise Exception, "Unknown note format '%s'" % note

    # Check for '#' and 'b' postfixes
    for post in note[1:]:
        if post == 'b':
            val -= 1
        elif post == '#':
            val += 1
    return val % 12

def remove_redundant_accidentals(note):
    """Removes redundant #'s and b's from the given note. 
    For example: C##b becomes C#, Eb##b becomes E, etc."""
    val = 0
    for token in note[1:]:
        if token == 'b':
            val -= 1
        elif token == '#':
            val += 1
    result = note[0]
    while val > 0:
        result = augment_note(result)
        val -= 1

    while val < 0:
        result = diminish_note(result)
        val += 1
    return result

def augment_note(note):
    if note[-1] != 'b':
        return note + '#'
    else:
        return note[:-1]

def diminish_note(note):
    if note[-1] != '#':
        return note + 'b'
    else:
        return note[:-1]

def measure_interval(note1, note2):
    """Returns an integer in the range of 0-11, determining the half note
    steps between note1 and note2."""
    res = note_to_int(note2) - note_to_int(note1)
    if res < 0:
        return 12 - (res * (-1))
    else:
        return res

def minor_major_helper(note1, note2, interval):
    """A helper function for the minor and major functions.
    You should probably not use this directly."""
    cur = measure_interval(note1, note2)
    while cur != interval:
        if cur > interval:
            note2 = diminish_note(note2)
        elif cur < interval:
            note2 = augment_note(note2)
        cur = measure_interval(note1, note2)

    # We are practically done right now, but we need to be able to create
    # the minor seventh of Cb and get Bbb instead of B######### as the result
    val = 0
    for token in note2[1:]:
        if token == '#':
            val += 1
        elif token == 'b':
            val -= 1

    # These are some checks to see if we have generated too much #'s
    # or too much b's. In these cases we need to convert #'s to b's
    # and vice versa. 
    if val > 6:
        val = val % 12
        val = -12 + val
    elif val < -6:
        val = val % -12
        val = 12 + val

    # Rebuild the note
    result = note2[0]
    while val > 0:
        result = augment_note(result)
        val -= 1
    while val < 0:
        result = diminish_note(result)
        val += 1
    return result
